iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Modern Web

後疫情時代的 WebRTC 微學習系列 第 23

Day23 [實作] 一對一視訊通話(3): Client

  • 分享至 

  • xImage
  •  

昨天我們把 Signaling server 完成了,今天我們要繼續完成 Client 端:

  1. 細部分解
  2. 完整程式碼
  3. 測試

https://ithelp.ithome.com.tw/upload/images/20211007/201300625LHZ59vYez.png

index.html

上一篇為了測試我們在 index.html 中寫入 hello,今天我們要把他替換掉,我們要在畫面中呈現兩個 video ,一個顯示自己的畫面,另一個顯示通話對方的畫面

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>1on1 webrtc</title>
  </head>
  <body>

    <div>
      <video muted="false" width="320" autoplay playsinline id="localVideo" ></video>
      <video width="320" autoplay playsinline id="remoteVideo"></video>
    </div>

    <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
    <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
    <script src="./js/main.js"></script>
    
  </body>
</html>

建立 js 檔

在 public 資料夾內建立一個 js 的資料夾,並在內部建立 main.js

// video 標籤
const localVideo = document.querySelector('video#localVideo')
const remoteVideo = document.querySelector('video#remoteVideo')

let localStream
let peerConn
const room = 'room1'

// socket
const socket = io('ws://0.0.0.0:8080')

socket.on('ready', async (msg) => {
	// TODO:- 收到 ready 代表對方已經連線,可以建立offer 發過去
})

socket.on('offer', async (desc) => {
  // TODO:- 收到 offer 後,設定對方的配置,並建立 answer 發送到對端
})

socket.on('answer', async (desc) => {
  // TODO:- 收到 answer 後,設定對方的配置
})

socket.on('ice_candidate', async (data) => {
  // TODO:- 加入新取得的 ICE candidate
})

function init() {
	// 加入房間
  socket.emit('join', room)
}

window.onload = init()

收到 ready 代表對方已經連線,可以建立offer 發過去

  1. 建立一個用來處理信令的 function

    /**
     * 處理信令
     * @param {Boolean} isOffer 是 offer 還是 answer
     */
    async function sendSDP(isOffer) {
      try {
        if (!peerConn) {
          return
        }
    
        // 建立 SDP 
        const localSDP = await peerConn[isOffer ? 'createOffer' : 'createAnswer']({
          offerToReceiveAudio: true,
          offerToReceiveVideo: true,
        })
    
        // 設定本地 SDP 信令
        await peerConn.setLocalDescription(localSDP)
    
        // 寄出SDP信令
        let e = isOffer ? 'offer' : 'answer'
        socket.emit(e, room, peerConn.localDescription)
      } catch (err) {
        throw err
      }
    }
    
  2. 在ready中加入

    socket.on('ready', async (msg) => {
      // 發送 offer
      await sendSDP(true)
    })
    

收到 offer 後,設定對方的配置,並建立 answer 發送到對端

socket.on('offer', async (desc) => {

  // 設定對方的配置
  await peerConn.setRemoteDescription(desc)

  // 發送 answer
  await sendSDP(false)
})

收到 answer 後,設定對方的配置

socket.on('answer', async (desc) => {

  // 設定對方的配置
  await peerConn.setRemoteDescription(desc)
})

加入新取得的 ICE candidate

socket.on('ice_candidate', async (data) => {

  const candidate = new RTCIceCandidate({
    sdpMLineIndex: data.label,
    candidate: data.candidate,
  })
  await peerConn.addIceCandidate(candidate)
})

取得自己的視訊並建立連接

/**
 * 取得本地串流
 */
async function createStream() {
  try {
    const stream = await navigator.mediaDevices.getUserMedia({
      audio: true,
      video: true,
    })

    localStream = stream

    localVideo.srcObject = stream
  } catch (err) {
    throw err
  }
}

/**
 * 初始化Peer連結
 */
function initPeerConnection() {
  const configuration = {
    iceServers: [
      {
        urls: 'stun:stun.l.google.com:19302',
      },
    ],
  }
  peerConn = new RTCPeerConnection(configuration)

  // 增加本地串流
  localStream.getTracks().forEach((track) => {
    peerConn.addTrack(track, localStream)
  })

  // 找尋到 ICE 候選位置後,送去 Server 與另一位配對
  peerConn.onicecandidate = (e) => {
    if (e.candidate) {
      console.log('發送 ICE')
      // 發送 ICE
      socket.emit('ice_candidate', room, {
        label: e.candidate.sdpMLineIndex,
        id: e.candidate.sdpMid,
        candidate: e.candidate.candidate,
      })
    }
  }

  // 監聽 ICE 連接狀態
  peerConn.oniceconnectionstatechange = (e) => {
    if (e.target.iceConnectionState === 'disconnected') {
      remoteVideo.srcObject = null
    }
  }

  // 監聽是否有流傳入,如果有的話就顯示影像
  peerConn.onaddstream = ({ stream }) => {
    // 接收流並顯示遠端視訊
    remoteVideo.srcObject = stream
  }
}
async function init() {
  await createStream()
  initPeerConnection()
  socket.emit('join', room)
}

完整程式碼如下:

  1. public/index.html

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>1on1 webrtc</title>
      </head>
      <body>
    
        <div>
          <video muted width="320" autoplay playsinline id="localVideo" ></video>
          <video width="320" autoplay playsinline id="remoteVideo"></video>
        </div>
    
        <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
        <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>
        <script src="./js/main.js"></script>
    
      </body>
    </html>
    
  2. pubilc/js/main.js

    // video 標籤
    const localVideo = document.querySelector('video#localVideo')
    const remoteVideo = document.querySelector('video#remoteVideo')
    
    let localStream
    let peerConn
    const room = 'room1'
    
    // socket
    const socket = io('ws://0.0.0.0:8080')
    
    socket.on('ready', async (msg) => {
      console.log(msg)
      // 發送 offer
      console.log('發送 offer ')
      await sendSDP(true)
    })
    
    socket.on('ice_candidate', async (data) => {
      console.log('收到 ice_candidate')
      const candidate = new RTCIceCandidate({
        sdpMLineIndex: data.label,
        candidate: data.candidate,
      })
      await peerConn.addIceCandidate(candidate)
    })
    
    socket.on('offer', async (desc) => {
      console.log('收到 offer')
    
      // 設定對方的配置
      await peerConn.setRemoteDescription(desc)
    
      // 發送 answer
      await sendSDP(false)
    })
    
    socket.on('answer', async (desc) => {
      console.log('收到 answer')
    
      // 設定對方的配置
      await peerConn.setRemoteDescription(desc)
    })
    
    /**
     * 取得本地串流
     */
    async function createStream() {
      try {
        // 取得影音的Stream
        const stream = await navigator.mediaDevices.getUserMedia({
          audio: true,
          video: true,
        })
    
        // 提升作用域
        localStream = stream
    
        // 導入<video>
        localVideo.srcObject = stream
      } catch (err) {
        throw err
      }
    }
    
    /**
     * 初始化Peer連結
     */
    function initPeerConnection() {
      const configuration = {
        iceServers: [
          {
            urls: 'stun:stun.l.google.com:19302',
          },
        ],
      }
      peerConn = new RTCPeerConnection(configuration)
    
      // 增加本地串流
      localStream.getTracks().forEach((track) => {
        peerConn.addTrack(track, localStream)
      })
    
      // 找尋到 ICE 候選位置後,送去 Server 與另一位配對
      peerConn.onicecandidate = (e) => {
        if (e.candidate) {
          console.log('發送 ICE')
          // 發送 ICE
          socket.emit('ice_candidate', room, {
            label: e.candidate.sdpMLineIndex,
            id: e.candidate.sdpMid,
            candidate: e.candidate.candidate,
          })
        }
      }
    
      // 監聽 ICE 連接狀態
      peerConn.oniceconnectionstatechange = (e) => {
        if (e.target.iceConnectionState === 'disconnected') {
          remoteVideo.srcObject = null
        }
      }
    
      // 監聽是否有流傳入,如果有的話就顯示影像
      peerConn.onaddstream = ({ stream }) => {
        // 接收流並顯示遠端視訊
        remoteVideo.srcObject = stream
      }
    }
    
    /**
     * 處理信令
     * @param {Boolean} isOffer 是 offer 還是 answer
     */
    async function sendSDP(isOffer) {
      try {
        if (!peerConn) {
          console.log('尚未開啟視訊')
          return
        }
    
        // 創建SDP信令
        const localSDP = await peerConn[isOffer ? 'createOffer' : 'createAnswer']({
          offerToReceiveAudio: true, // 是否傳送聲音流給對方
          offerToReceiveVideo: true, // 是否傳送影像流給對方
        })
    
        // 設定本地SDP信令
        await peerConn.setLocalDescription(localSDP)
    
        // 寄出SDP信令
        let e = isOffer ? 'offer' : 'answer'
        socket.emit(e, room, peerConn.localDescription)
      } catch (err) {
        throw err
      }
    }
    
    /**
     * 初始化
     */
    async function init() {
      await createStream()
      initPeerConnection()
      socket.emit('join', room)
    }
    
    window.onload = init()
    

測試

  1. 確認結構

    ❯ tree -I 'node_modules'
    .
    ├── package-lock.json
    ├── package.json
    ├── public
    │   ├── index.html
    │   ├── index.js
    │   └── js
    │       └── main.js
    └── server.js
    
    2 directories, 6 files
    
  2. 進入 1-on-1-webrtc 資料夾內部

    node server.js
    
  3. 連線開啟兩個視窗 PC_A 及 PC_B

    http://localhost:8080/
    

    https://ithelp.ithome.com.tw/upload/images/20211007/20130062e38Wjrughv.png


上一篇
Day22 [實作] 一對一視訊通話(2): Signaling server
下一篇
Day24 [實作] 一對一視訊通話(4): 加入通話及掛斷機制
系列文
後疫情時代的 WebRTC 微學習30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言